Skip to content

Fix pre-auth double-free in JSON request parser#539

Open
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix/json-request-double-free
Open

Fix pre-auth double-free in JSON request parser#539
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix/json-request-double-free

Conversation

@gigioneggiando

Copy link
Copy Markdown

Summary

json_string() returns false on a non-string or malformed value without writing *out. Several callers reparse in place with free(x); json_string(&p, &x):

  • the "model" field in parse_chat_request, parse_completion_request, and parse_anthropic_request;
  • duplicate JSON keys for type / name / description / order.name across the tool-schema and message parsers (~12 sites in total).

On a failed reparse, *out keeps the just-freed pointer (dangling, not NULL). The request's cleanup path (request_free) then free()s it a second time — a double-free of a small heap allocation, reachable pre-auth with a single request:

POST /v1/chat/completions
{"messages":[{"role":"user","content":"hi"}],"model":0}

(also via /v1/messages, /v1/responses, /v1/completions; and a duplicate-key variant {"content":"ok","content":0}).

Fix

Initialize *out = NULL at the top of json_string, so every failure path leaves the caller's pointer defined. This closes all the free(x); json_string(&p, &x) sites at the root instead of patching each call site, and matches the pattern the parser already uses where it nulls after freeing. Behavior on the success path is unchanged (*out is still overwritten by buf_take), so valid requests parse exactly as before.

Verification

Built the parser under AddressSanitizer/UBSan (driving the real parse_chat_request, unmodified):

  • before: AddressSanitizer: attempting double-free — alloc in xstrdup, first free at the "model" handler, second free in request_free via the bad: label.
  • after: the malformed request is cleanly rejected (invalid JSON request), no sanitizer error.

Found during a coordinated security review of ds4; a standalone offline PoC is available on request. Happy to adjust the approach (e.g. per-call-site nulling) if you prefer.

json_string() returns false on a non-string or malformed value without
writing *out. Several callers reparse in place with
`free(x); json_string(&p, &x)` -- the "model" field in
parse_chat_request / parse_completion_request / parse_anthropic_request,
and duplicate JSON keys for type/name/description across the tool-schema
and message parsers. On a failed reparse, *out keeps the just-freed
pointer; the request's cleanup (request_free) then frees it again.

This is reachable pre-auth with a single request, e.g.
  POST /v1/chat/completions  {"messages":[...],"model":0}

Initialize *out = NULL at the top of json_string so every failure path
leaves the caller's pointer defined, closing all the free-then-reparse
sites at the root rather than patching each call site. The success path
is unchanged (*out is still set by buf_take).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant